/**
 * Copyright 2010-present Facebook.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook.widget;

import android.app.Activity;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.view.animation.AlphaAnimation;
import android.widget.*;
import com.facebook.FacebookException;
import com.facebook.Request;
import com.facebook.Session;
import com.facebook.SessionState;
import com.facebook.android.R;
import com.facebook.internal.SessionTracker;
import com.facebook.model.GraphObject;

import java.util.*;

/**
 * Provides functionality common to SDK UI elements that allow the user to pick one or more
 * graph objects (e.g., places, friends) from a list of possibilities. The UI is exposed as a
 * Fragment to allow to it to be included in an Activity along with other Fragments. The Fragments
 * can be configured by passing parameters as part of their Intent bundle, or (for certain
 * properties) by specifying attributes in their XML layout files.
 * <br/>
 * PickerFragments support callbacks that will be called in the event of an error, when the
 * underlying data has been changed, or when the set of selected graph objects changes.
 */
public abstract class PickerFragment<T extends GraphObject> extends Fragment
{
  /**
   * The key for a boolean parameter in the fragment's Intent bundle to indicate whether the
   * picker should show pictures (if available) for the graph objects.
   */
  public static final String SHOW_PICTURES_BUNDLE_KEY = "com.facebook.widget.PickerFragment.ShowPictures";
  /**
   * The key for a String parameter in the fragment's Intent bundle to indicate which extra fields
   * beyond the default fields should be retrieved for any graph objects in the results.
   */
  public static final String EXTRA_FIELDS_BUNDLE_KEY = "com.facebook.widget.PickerFragment.ExtraFields";
  /**
   * The key for a boolean parameter in the fragment's Intent bundle to indicate whether the
   * picker should display a title bar with a Done button.
   */
  public static final String SHOW_TITLE_BAR_BUNDLE_KEY = "com.facebook.widget.PickerFragment.ShowTitleBar";
  /**
   * The key for a String parameter in the fragment's Intent bundle to indicate the text to
   * display in the title bar.
   */
  public static final String TITLE_TEXT_BUNDLE_KEY = "com.facebook.widget.PickerFragment.TitleText";
  /**
   * The key for a String parameter in the fragment's Intent bundle to indicate the text to
   * display in the Done btuton.
   */
  public static final String DONE_BUTTON_TEXT_BUNDLE_KEY = "com.facebook.widget.PickerFragment.DoneButtonText";

  private static final String SELECTION_BUNDLE_KEY = "com.facebook.android.PickerFragment.Selection";
  private static final String ACTIVITY_CIRCLE_SHOW_KEY = "com.facebook.android.PickerFragment.ActivityCircleShown";
  private static final int PROFILE_PICTURE_PREFETCH_BUFFER = 5;

  private final int layout;
  private OnErrorListener onErrorListener;
  private OnDataChangedListener onDataChangedListener;
  private OnSelectionChangedListener onSelectionChangedListener;
  private OnDoneButtonClickedListener onDoneButtonClickedListener;
  private GraphObjectFilter<T> filter;
  private boolean showPictures = true;
  private boolean showTitleBar = true;
  private ListView listView;
  HashSet<String> extraFields = new HashSet<String>();
  GraphObjectAdapter<T> adapter;
  private final Class<T> graphObjectClass;
  private LoadingStrategy loadingStrategy;
  private SelectionStrategy selectionStrategy;
  private ProgressBar activityCircle;
  private SessionTracker sessionTracker;
  private String titleText;
  private String doneButtonText;
  private TextView titleTextView;
  private Button doneButton;
  private Drawable titleBarBackground;
  private Drawable doneButtonBackground;
  private boolean appEventsLogged;

  PickerFragment(Class<T> graphObjectClass, int layout, Bundle args)
  {
    this.graphObjectClass = graphObjectClass;
    this.layout = layout;

    setPickerFragmentSettingsFromBundle(args);
  }

  @Override
  public void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);

    adapter = createAdapter();
    adapter.setFilter(new GraphObjectAdapter.Filter<T>()
    {
      @Override
      public boolean includeItem(T graphObject)
      {
        return filterIncludesItem(graphObject);
      }
    });
  }

  @Override
  public void onInflate(Activity activity, AttributeSet attrs, Bundle savedInstanceState)
  {
    super.onInflate(activity, attrs, savedInstanceState);
    TypedArray a = activity.obtainStyledAttributes(attrs, R.styleable.com_facebook_picker_fragment);

    setShowPictures(a.getBoolean(R.styleable.com_facebook_picker_fragment_show_pictures, showPictures));
    String extraFieldsString = a.getString(R.styleable.com_facebook_picker_fragment_extra_fields);
    if (extraFieldsString != null)
    {
      String[] strings = extraFieldsString.split(",");
      setExtraFields(Arrays.asList(strings));
    }

    showTitleBar = a.getBoolean(R.styleable.com_facebook_picker_fragment_show_title_bar, showTitleBar);
    titleText = a.getString(R.styleable.com_facebook_picker_fragment_title_text);
    doneButtonText = a.getString(R.styleable.com_facebook_picker_fragment_done_button_text);
    titleBarBackground = a.getDrawable(R.styleable.com_facebook_picker_fragment_title_bar_background);
    doneButtonBackground = a.getDrawable(R.styleable.com_facebook_picker_fragment_done_button_background);

    a.recycle();
  }

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
  {
    ViewGroup view = (ViewGroup) inflater.inflate(layout, container, false);

    listView = (ListView) view.findViewById(R.id.com_facebook_picker_list_view);
    listView.setOnItemClickListener(new AdapterView.OnItemClickListener()
    {
      @Override
      public void onItemClick(AdapterView<?> parent, View v, int position, long id)
      {
        onListItemClick((ListView) parent, v, position);
      }
    });
    listView.setOnLongClickListener(new View.OnLongClickListener()
    {
      @Override
      public boolean onLongClick(View v)
      {
        // We don't actually do anything differently on long-clicks, but setting the listener
        // enables the selector transition that we have for visual consistency with the
        // Facebook app's pickers.
        return false;
      }
    });
    listView.setOnScrollListener(onScrollListener);

    activityCircle = (ProgressBar) view.findViewById(R.id.com_facebook_picker_activity_circle);

    setupViews(view);

    listView.setAdapter(adapter);

    return view;
  }

  @Override
  public void onActivityCreated(final Bundle savedInstanceState)
  {
    super.onActivityCreated(savedInstanceState);

    sessionTracker = new SessionTracker(getActivity(), new Session.StatusCallback()
    {
      @Override
      public void call(Session session, SessionState state, Exception exception)
      {
        if (!session.isOpened())
        {
          // When a session is closed, we want to clear out our data so it is not visible to subsequent users
          clearResults();
        }
      }
    });

    setSettingsFromBundle(savedInstanceState);

    loadingStrategy = createLoadingStrategy();
    loadingStrategy.attach(adapter);

    selectionStrategy = createSelectionStrategy();
    selectionStrategy.readSelectionFromBundle(savedInstanceState, SELECTION_BUNDLE_KEY);

    // Should we display a title bar? (We need to do this after we've retrieved our bundle settings.)
    if (showTitleBar)
    {
      inflateTitleBar((ViewGroup) getView());
    }

    if (activityCircle != null && savedInstanceState != null)
    {
      boolean shown = savedInstanceState.getBoolean(ACTIVITY_CIRCLE_SHOW_KEY, false);
      if (shown)
      {
        displayActivityCircle();
      }
      else
      {
        // Should be hidden already, but just to be sure.
        hideActivityCircle();
      }
    }
  }

  @Override
  public void onDetach()
  {
    super.onDetach();

    listView.setOnScrollListener(null);
    listView.setAdapter(null);

    loadingStrategy.detach();
    sessionTracker.stopTracking();
  }

  @Override
  public void onSaveInstanceState(Bundle outState)
  {
    super.onSaveInstanceState(outState);

    saveSettingsToBundle(outState);
    selectionStrategy.saveSelectionToBundle(outState, SELECTION_BUNDLE_KEY);
    if (activityCircle != null)
    {
      outState.putBoolean(ACTIVITY_CIRCLE_SHOW_KEY, activityCircle.getVisibility() == View.VISIBLE);
    }
  }

  @Override
  public void onStop()
  {
    if (!appEventsLogged)
    {
      logAppEvents(false);
    }
    super.onStop();
  }

  @Override
  public void setArguments(Bundle args)
  {
    super.setArguments(args);
    setSettingsFromBundle(args);
  }

  /**
   * Gets the current OnDataChangedListener for this fragment, which will be called whenever
   * the underlying data being displaying in the picker has changed.
   *
   * @return the OnDataChangedListener, or null if there is none
   */
  public OnDataChangedListener getOnDataChangedListener()
  {
    return onDataChangedListener;
  }

  /**
   * Sets the current OnDataChangedListener for this fragment, which will be called whenever
   * the underlying data being displaying in the picker has changed.
   *
   * @param onDataChangedListener the OnDataChangedListener, or null if there is none
   */
  public void setOnDataChangedListener(OnDataChangedListener onDataChangedListener)
  {
    this.onDataChangedListener = onDataChangedListener;
  }

  /**
   * Gets the current OnSelectionChangedListener for this fragment, which will be called
   * whenever the user selects or unselects a graph object in the list.
   *
   * @return the OnSelectionChangedListener, or null if there is none
   */
  public OnSelectionChangedListener getOnSelectionChangedListener()
  {
    return onSelectionChangedListener;
  }

  /**
   * Sets the current OnSelectionChangedListener for this fragment, which will be called
   * whenever the user selects or unselects a graph object in the list.
   *
   * @param onSelectionChangedListener the OnSelectionChangedListener, or null if there is none
   */
  public void setOnSelectionChangedListener(
      OnSelectionChangedListener onSelectionChangedListener)
  {
    this.onSelectionChangedListener = onSelectionChangedListener;
  }

  /**
   * Gets the current OnDoneButtonClickedListener for this fragment, which will be called
   * when the user clicks the Done button.
   *
   * @return the OnDoneButtonClickedListener, or null if there is none
   */
  public OnDoneButtonClickedListener getOnDoneButtonClickedListener()
  {
    return onDoneButtonClickedListener;
  }

  /**
   * Sets the current OnDoneButtonClickedListener for this fragment, which will be called
   * when the user clicks the Done button. This will only be possible if the title bar is
   * being shown in this fragment.
   *
   * @param onDoneButtonClickedListener the OnDoneButtonClickedListener, or null if there is none
   */
  public void setOnDoneButtonClickedListener(OnDoneButtonClickedListener onDoneButtonClickedListener)
  {
    this.onDoneButtonClickedListener = onDoneButtonClickedListener;
  }

  /**
   * Gets the current OnErrorListener for this fragment, which will be called in the event
   * of network or other errors encountered while populating the graph objects in the list.
   *
   * @return the OnErrorListener, or null if there is none
   */
  public OnErrorListener getOnErrorListener()
  {
    return onErrorListener;
  }

  /**
   * Sets the current OnErrorListener for this fragment, which will be called in the event
   * of network or other errors encountered while populating the graph objects in the list.
   *
   * @param onErrorListener the OnErrorListener, or null if there is none
   */
  public void setOnErrorListener(OnErrorListener onErrorListener)
  {
    this.onErrorListener = onErrorListener;
  }

  /**
   * Gets the current filter for this fragment, which will be called for each graph object
   * returned from the service to determine if it should be displayed in the list.
   * If no filter is specified, all retrieved graph objects will be displayed.
   *
   * @return the GraphObjectFilter, or null if there is none
   */
  public GraphObjectFilter<T> getFilter()
  {
    return filter;
  }

  /**
   * Sets the current filter for this fragment, which will be called for each graph object
   * returned from the service to determine if it should be displayed in the list.
   * If no filter is specified, all retrieved graph objects will be displayed.
   *
   * @param filter the GraphObjectFilter, or null if there is none
   */
  public void setFilter(GraphObjectFilter<T> filter)
  {
    this.filter = filter;
  }

  /**
   * Gets the Session to use for any Facebook requests this fragment will make.
   *
   * @return the Session that will be used for any Facebook requests, or null if there is none
   */
  public Session getSession()
  {
    return sessionTracker.getSession();
  }

  /**
   * Sets the Session to use for any Facebook requests this fragment will make. If the
   * parameter is null, the fragment will use the current active session, if any.
   *
   * @param session the Session to use for Facebook requests, or null to use the active session
   */
  public void setSession(Session session)
  {
    sessionTracker.setSession(session);
  }

  /**
   * Gets whether to display pictures, if available, for displayed graph objects.
   *
   * @return true if pictures should be displayed, false if not
   */
  public boolean getShowPictures()
  {
    return showPictures;
  }

  /**
   * Sets whether to display pictures, if available, for displayed graph objects.
   *
   * @param showPictures true if pictures should be displayed, false if not
   */
  public void setShowPictures(boolean showPictures)
  {
    this.showPictures = showPictures;
  }

  /**
   * Gets the extra fields to request for the retrieved graph objects.
   *
   * @return the extra fields to request
   */
  public Set<String> getExtraFields()
  {
    return new HashSet<String>(extraFields);
  }

  /**
   * Sets the extra fields to request for the retrieved graph objects.
   *
   * @param fields the extra fields to request
   */
  public void setExtraFields(Collection<String> fields)
  {
    extraFields = new HashSet<String>();
    if (fields != null)
    {
      extraFields.addAll(fields);
    }
  }

  /**
   * Sets whether to show a title bar with a Done button. This must be
   * called prior to the Fragment going through its creation lifecycle to have an effect.
   *
   * @param showTitleBar true if a title bar should be displayed, false if not
   */
  public void setShowTitleBar(boolean showTitleBar)
  {
    this.showTitleBar = showTitleBar;
  }

  /**
   * Gets whether to show a title bar with a Done button. The default is true.
   *
   * @return true if a title bar will be shown, false if not.
   */
  public boolean getShowTitleBar()
  {
    return showTitleBar;
  }

  /**
   * Sets the text to show in the title bar, if a title bar is to be shown. This must be
   * called prior to the Fragment going through its creation lifecycle to have an effect, or
   * the default will be used.
   *
   * @param titleText the text to show in the title bar
   */
  public void setTitleText(String titleText)
  {
    this.titleText = titleText;
  }

  /**
   * Gets the text to show in the title bar, if a title bar is to be shown.
   *
   * @return the text to show in the title bar
   */
  public String getTitleText()
  {
    if (titleText == null)
    {
      titleText = getDefaultTitleText();
    }
    return titleText;
  }

  /**
   * Sets the text to show in the Done button, if a title bar is to be shown. This must be
   * called prior to the Fragment going through its creation lifecycle to have an effect, or
   * the default will be used.
   *
   * @param doneButtonText the text to show in the Done button
   */
  public void setDoneButtonText(String doneButtonText)
  {
    this.doneButtonText = doneButtonText;
  }

  /**
   * Gets the text to show in the Done button, if a title bar is to be shown.
   *
   * @return the text to show in the Done button
   */
  public String getDoneButtonText()
  {
    if (doneButtonText == null)
    {
      doneButtonText = getDefaultDoneButtonText();
    }
    return doneButtonText;
  }

  /**
   * Causes the picker to load data from the service and display it to the user.
   *
   * @param forceReload if true, data will be loaded even if there is already data being displayed (or loading);
   *                    if false, data will not be re-loaded if it is already displayed (or loading)
   */
  public void loadData(boolean forceReload)
  {
    if (!forceReload && loadingStrategy.isDataPresentOrLoading())
    {
      return;
    }
    loadDataSkippingRoundTripIfCached();
  }

  /**
   * Updates the properties of the PickerFragment based on the contents of the supplied Bundle;
   * calling Activities may use this to pass additional configuration information to the
   * PickerFragment beyond what is specified in its XML layout.
   *
   * @param inState a Bundle containing keys corresponding to properties of the PickerFragment
   */
  public void setSettingsFromBundle(Bundle inState)
  {
    setPickerFragmentSettingsFromBundle(inState);
  }

  void setupViews(ViewGroup view)
  {
  }

  boolean filterIncludesItem(T graphObject)
  {
    if (filter != null)
    {
      return filter.includeItem(graphObject);
    }
    return true;
  }

  List<T> getSelectedGraphObjects()
  {
    return adapter.getGraphObjectsById(selectionStrategy.getSelectedIds());
  }

  void setSelectedGraphObjects(List<String> objectIds)
  {
    for (String objectId : objectIds)
    {
      if (!this.selectionStrategy.isSelected(objectId))
      {
        this.selectionStrategy.toggleSelection(objectId);
      }
    }
  }

  void saveSettingsToBundle(Bundle outState)
  {
    outState.putBoolean(SHOW_PICTURES_BUNDLE_KEY, showPictures);
    if (!extraFields.isEmpty())
    {
      outState.putString(EXTRA_FIELDS_BUNDLE_KEY, TextUtils.join(",", extraFields));
    }
    outState.putBoolean(SHOW_TITLE_BAR_BUNDLE_KEY, showTitleBar);
    outState.putString(TITLE_TEXT_BUNDLE_KEY, titleText);
    outState.putString(DONE_BUTTON_TEXT_BUNDLE_KEY, doneButtonText);
  }

  abstract Request getRequestForLoadData(Session session);

  abstract PickerFragmentAdapter<T> createAdapter();

  abstract LoadingStrategy createLoadingStrategy();

  abstract SelectionStrategy createSelectionStrategy();

  void onLoadingData()
  {
  }

  String getDefaultTitleText()
  {
    return null;
  }

  String getDefaultDoneButtonText()
  {
    return getString(R.string.com_facebook_picker_done_button_text);
  }

  void displayActivityCircle()
  {
    if (activityCircle != null)
    {
      layoutActivityCircle();
      activityCircle.setVisibility(View.VISIBLE);
    }
  }

  void layoutActivityCircle()
  {
    // If we've got no data, make the activity circle full-opacity. Otherwise we'll dim it to avoid
    //  cluttering the UI.
    float alpha = (!adapter.isEmpty()) ? .25f : 1.0f;
    setAlpha(activityCircle, alpha);
  }

  void hideActivityCircle()
  {
    if (activityCircle != null)
    {
      // We use an animation to dim the activity circle; need to clear this or it will remain visible.
      activityCircle.clearAnimation();
      activityCircle.setVisibility(View.INVISIBLE);
    }
  }

  void setSelectionStrategy(SelectionStrategy selectionStrategy)
  {
    if (selectionStrategy != this.selectionStrategy)
    {
      this.selectionStrategy = selectionStrategy;
      if (adapter != null)
      {
        // Adapter should cause a re-render.
        adapter.notifyDataSetChanged();
      }
    }
  }

  void logAppEvents(boolean doneButtonClicked)
  {
  }

  private static void setAlpha(View view, float alpha)
  {
    // Set the alpha appropriately (setAlpha is API >= 11, this technique works on all API levels).
    AlphaAnimation alphaAnimation = new AlphaAnimation(alpha, alpha);
    alphaAnimation.setDuration(0);
    alphaAnimation.setFillAfter(true);
    view.startAnimation(alphaAnimation);
  }


  private void setPickerFragmentSettingsFromBundle(Bundle inState)
  {
    // We do this in a separate non-overridable method so it is safe to call from the constructor.
    if (inState != null)
    {
      showPictures = inState.getBoolean(SHOW_PICTURES_BUNDLE_KEY, showPictures);
      String extraFieldsString = inState.getString(EXTRA_FIELDS_BUNDLE_KEY);
      if (extraFieldsString != null)
      {
        String[] strings = extraFieldsString.split(",");
        setExtraFields(Arrays.asList(strings));
      }
      showTitleBar = inState.getBoolean(SHOW_TITLE_BAR_BUNDLE_KEY, showTitleBar);
      String titleTextString = inState.getString(TITLE_TEXT_BUNDLE_KEY);
      if (titleTextString != null)
      {
        titleText = titleTextString;
        if (titleTextView != null)
        {
          titleTextView.setText(titleText);
        }
      }
      String doneButtonTextString = inState.getString(DONE_BUTTON_TEXT_BUNDLE_KEY);
      if (doneButtonTextString != null)
      {
        doneButtonText = doneButtonTextString;
        if (doneButton != null)
        {
          doneButton.setText(doneButtonText);
        }
      }
    }
  }

  private void inflateTitleBar(ViewGroup view)
  {
    ViewStub stub = (ViewStub) view.findViewById(R.id.com_facebook_picker_title_bar_stub);
    if (stub != null)
    {
      View titleBar = stub.inflate();

      final RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
          RelativeLayout.LayoutParams.MATCH_PARENT,
          RelativeLayout.LayoutParams.MATCH_PARENT);
      layoutParams.addRule(RelativeLayout.BELOW, R.id.com_facebook_picker_title_bar);
      listView.setLayoutParams(layoutParams);

      if (titleBarBackground != null)
      {
        titleBar.setBackgroundDrawable(titleBarBackground);
      }

      doneButton = (Button) view.findViewById(R.id.com_facebook_picker_done_button);
      if (doneButton != null)
      {
        doneButton.setOnClickListener(new View.OnClickListener()
        {
          @Override
          public void onClick(View v)
          {
            logAppEvents(true);
            appEventsLogged = true;

            if (onDoneButtonClickedListener != null)
            {
              onDoneButtonClickedListener.onDoneButtonClicked(PickerFragment.this);
            }
          }
        });

        if (getDoneButtonText() != null)
        {
          doneButton.setText(getDoneButtonText());
        }

        if (doneButtonBackground != null)
        {
          doneButton.setBackgroundDrawable(doneButtonBackground);
        }
      }

      titleTextView = (TextView) view.findViewById(R.id.com_facebook_picker_title);
      if (titleTextView != null)
      {
        if (getTitleText() != null)
        {
          titleTextView.setText(getTitleText());
        }
      }
    }
  }

  private void onListItemClick(ListView listView, View v, int position)
  {
    @SuppressWarnings("unchecked")
    T graphObject = (T) listView.getItemAtPosition(position);
    String id = adapter.getIdOfGraphObject(graphObject);
    selectionStrategy.toggleSelection(id);
    adapter.notifyDataSetChanged();

    if (onSelectionChangedListener != null)
    {
      onSelectionChangedListener.onSelectionChanged(PickerFragment.this);
    }
  }

  private void loadDataSkippingRoundTripIfCached()
  {
    clearResults();

    Request request = getRequestForLoadData(getSession());
    if (request != null)
    {
      onLoadingData();
      loadingStrategy.startLoading(request);
    }
  }

  private void clearResults()
  {
    if (adapter != null)
    {
      boolean wasSelection = !selectionStrategy.isEmpty();
      boolean wasData = !adapter.isEmpty();

      loadingStrategy.clearResults();
      selectionStrategy.clear();
      adapter.notifyDataSetChanged();

      // Tell anyone who cares the data and selection has changed, if they have.
      if (wasData && onDataChangedListener != null)
      {
        onDataChangedListener.onDataChanged(PickerFragment.this);
      }
      if (wasSelection && onSelectionChangedListener != null)
      {
        onSelectionChangedListener.onSelectionChanged(PickerFragment.this);
      }
    }
  }

  void updateAdapter(SimpleGraphObjectCursor<T> data)
  {
    if (adapter != null)
    {
      // As we fetch additional results and add them to the table, we do not
      // want the items displayed jumping around seemingly at random, frustrating the user's
      // attempts at scrolling, etc. Since results may be added anywhere in
      // the table, we choose to try to keep the first visible row in a fixed
      // position (from the user's perspective). We try to keep it positioned at
      // the same offset from the top of the screen so adding new items seems
      // smoother, as opposed to having it "snap" to a multiple of row height

      // We use the second row, to give context above and below it and avoid
      // cases where the first row is only barely visible, thus providing little context.
      // The exception is where the very first row is visible, in which case we use that.
      View view = listView.getChildAt(1);
      int anchorPosition = listView.getFirstVisiblePosition();
      if (anchorPosition > 0)
      {
        anchorPosition++;
      }
      GraphObjectAdapter.SectionAndItem<T> anchorItem = adapter.getSectionAndItem(anchorPosition);
      final int top = (view != null &&
          anchorItem.getType() != GraphObjectAdapter.SectionAndItem.Type.ACTIVITY_CIRCLE) ?
          view.getTop() : 0;

      // Now actually add the results.
      boolean dataChanged = adapter.changeCursor(data);

      if (view != null && anchorItem != null)
      {
        // Put the item back in the same spot it was.
        final int newPositionOfItem = adapter.getPosition(anchorItem.sectionKey, anchorItem.graphObject);
        if (newPositionOfItem != -1)
        {
          listView.setSelectionFromTop(newPositionOfItem, top);
        }
      }

      if (dataChanged && onDataChangedListener != null)
      {
        onDataChangedListener.onDataChanged(PickerFragment.this);
      }
    }
  }

  private void reprioritizeDownloads()
  {
    int lastVisibleItem = listView.getLastVisiblePosition();
    if (lastVisibleItem >= 0)
    {
      int firstVisibleItem = listView.getFirstVisiblePosition();
      adapter.prioritizeViewRange(firstVisibleItem, lastVisibleItem, PROFILE_PICTURE_PREFETCH_BUFFER);
    }
  }

  private ListView.OnScrollListener onScrollListener = new ListView.OnScrollListener()
  {
    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState)
    {
    }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)
    {
      reprioritizeDownloads();
    }
  };

  /**
   * Callback interface that will be called when a network or other error is encountered
   * while retrieving graph objects.
   */
  public interface OnErrorListener
  {
    /**
     * Called when a network or other error is encountered.
     *
     * @param error a FacebookException representing the error that was encountered.
     */
    void onError(PickerFragment<?> fragment, FacebookException error);
  }

  /**
   * Callback interface that will be called when the underlying data being displayed in the
   * picker has been updated.
   */
  public interface OnDataChangedListener
  {
    /**
     * Called when the set of data being displayed in the picker has changed.
     */
    void onDataChanged(PickerFragment<?> fragment);
  }

  /**
   * Callback interface that will be called when the user selects or unselects graph objects
   * in the picker.
   */
  public interface OnSelectionChangedListener
  {
    /**
     * Called when the user selects or unselects graph objects in the picker.
     */
    void onSelectionChanged(PickerFragment<?> fragment);
  }

  /**
   * Callback interface that will be called when the user clicks the Done button on the
   * title bar.
   */
  public interface OnDoneButtonClickedListener
  {
    /**
     * Called when the user clicks the Done button.
     */
    void onDoneButtonClicked(PickerFragment<?> fragment);
  }

  /**
   * Callback interface that will be called to determine if a graph object should be displayed.
   *
   * @param <T>
   */
  public interface GraphObjectFilter<T>
  {
    /**
     * Called to determine if a graph object should be displayed.
     *
     * @param graphObject the graph object
     * @return true to display the graph object, false to hide it
     */
    boolean includeItem(T graphObject);
  }

  abstract class LoadingStrategy
  {
    protected final static int CACHED_RESULT_REFRESH_DELAY = 2 * 1000;

    protected GraphObjectPagingLoader<T> loader;
    protected GraphObjectAdapter<T> adapter;

    public void attach(GraphObjectAdapter<T> adapter)
    {
      loader = (GraphObjectPagingLoader<T>) getLoaderManager().initLoader(0, null,
          new LoaderManager.LoaderCallbacks<SimpleGraphObjectCursor<T>>()
          {
            @Override
            public Loader<SimpleGraphObjectCursor<T>> onCreateLoader(int id, Bundle args)
            {
              return LoadingStrategy.this.onCreateLoader();
            }

            @Override
            public void onLoadFinished(Loader<SimpleGraphObjectCursor<T>> loader,
                                       SimpleGraphObjectCursor<T> data)
            {
              if (loader != LoadingStrategy.this.loader)
              {
                throw new FacebookException("Received callback for unknown loader.");
              }
              LoadingStrategy.this.onLoadFinished((GraphObjectPagingLoader<T>) loader, data);
            }

            @Override
            public void onLoaderReset(Loader<SimpleGraphObjectCursor<T>> loader)
            {
              if (loader != LoadingStrategy.this.loader)
              {
                throw new FacebookException("Received callback for unknown loader.");
              }
              LoadingStrategy.this.onLoadReset((GraphObjectPagingLoader<T>) loader);
            }
          });

      loader.setOnErrorListener(new GraphObjectPagingLoader.OnErrorListener()
      {
        @Override
        public void onError(FacebookException error, GraphObjectPagingLoader<?> loader)
        {
          hideActivityCircle();
          if (onErrorListener != null)
          {
            onErrorListener.onError(PickerFragment.this, error);
          }
        }
      });

      this.adapter = adapter;
      // Tell the adapter about any data we might already have.
      this.adapter.changeCursor(loader.getCursor());
      this.adapter.setOnErrorListener(new GraphObjectAdapter.OnErrorListener()
      {
        @Override
        public void onError(GraphObjectAdapter<?> adapter, FacebookException error)
        {
          if (onErrorListener != null)
          {
            onErrorListener.onError(PickerFragment.this, error);
          }
        }
      });
    }

    public void detach()
    {
      adapter.setDataNeededListener(null);
      adapter.setOnErrorListener(null);
      loader.setOnErrorListener(null);

      loader = null;
      adapter = null;
    }

    public void clearResults()
    {
      if (loader != null)
      {
        loader.clearResults();
      }
    }

    public void startLoading(Request request)
    {
      if (loader != null)
      {
        loader.startLoading(request, true);
        onStartLoading(loader, request);
      }
    }

    public boolean isDataPresentOrLoading()
    {
      return !adapter.isEmpty() || loader.isLoading();
    }

    protected GraphObjectPagingLoader<T> onCreateLoader()
    {
      return new GraphObjectPagingLoader<T>(getActivity(), graphObjectClass);
    }

    protected void onStartLoading(GraphObjectPagingLoader<T> loader, Request request)
    {
      displayActivityCircle();
    }

    protected void onLoadReset(GraphObjectPagingLoader<T> loader)
    {
      adapter.changeCursor(null);
    }

    protected void onLoadFinished(GraphObjectPagingLoader<T> loader, SimpleGraphObjectCursor<T> data)
    {
      updateAdapter(data);
    }
  }

  abstract class SelectionStrategy
  {
    abstract boolean isSelected(String id);

    abstract void toggleSelection(String id);

    abstract Collection<String> getSelectedIds();

    abstract void clear();

    abstract boolean isEmpty();

    abstract boolean shouldShowCheckBoxIfUnselected();

    abstract void saveSelectionToBundle(Bundle outBundle, String key);

    abstract void readSelectionFromBundle(Bundle inBundle, String key);
  }

  class SingleSelectionStrategy extends SelectionStrategy
  {
    private String selectedId;

    public Collection<String> getSelectedIds()
    {
      return Arrays.asList(new String[]{selectedId});
    }

    @Override
    boolean isSelected(String id)
    {
      return selectedId != null && id != null && selectedId.equals(id);
    }

    @Override
    void toggleSelection(String id)
    {
      if (selectedId != null && selectedId.equals(id))
      {
        selectedId = null;
      }
      else
      {
        selectedId = id;
      }
    }

    @Override
    void saveSelectionToBundle(Bundle outBundle, String key)
    {
      if (!TextUtils.isEmpty(selectedId))
      {
        outBundle.putString(key, selectedId);
      }
    }

    @Override
    void readSelectionFromBundle(Bundle inBundle, String key)
    {
      if (inBundle != null)
      {
        selectedId = inBundle.getString(key);
      }
    }

    @Override
    public void clear()
    {
      selectedId = null;
    }

    @Override
    boolean isEmpty()
    {
      return selectedId == null;
    }

    @Override
    boolean shouldShowCheckBoxIfUnselected()
    {
      return false;
    }
  }

  class MultiSelectionStrategy extends SelectionStrategy
  {
    private Set<String> selectedIds = new HashSet<String>();

    public Collection<String> getSelectedIds()
    {
      return selectedIds;
    }

    @Override
    boolean isSelected(String id)
    {
      return id != null && selectedIds.contains(id);
    }

    @Override
    void toggleSelection(String id)
    {
      if (id != null)
      {
        if (selectedIds.contains(id))
        {
          selectedIds.remove(id);
        }
        else
        {
          selectedIds.add(id);
        }
      }
    }

    @Override
    void saveSelectionToBundle(Bundle outBundle, String key)
    {
      if (!selectedIds.isEmpty())
      {
        String ids = TextUtils.join(",", selectedIds);
        outBundle.putString(key, ids);
      }
    }

    @Override
    void readSelectionFromBundle(Bundle inBundle, String key)
    {
      if (inBundle != null)
      {
        String ids = inBundle.getString(key);
        if (ids != null)
        {
          String[] splitIds = TextUtils.split(ids, ",");
          selectedIds.clear();
          Collections.addAll(selectedIds, splitIds);
        }
      }
    }

    @Override
    public void clear()
    {
      selectedIds.clear();
    }

    @Override
    boolean isEmpty()
    {
      return selectedIds.isEmpty();
    }

    @Override
    boolean shouldShowCheckBoxIfUnselected()
    {
      return true;
    }
  }

  abstract class PickerFragmentAdapter<U extends GraphObject> extends GraphObjectAdapter<T>
  {
    public PickerFragmentAdapter(Context context)
    {
      super(context);
    }

    @Override
    boolean isGraphObjectSelected(String graphObjectId)
    {
      return selectionStrategy.isSelected(graphObjectId);
    }

    @Override
    void updateCheckboxState(CheckBox checkBox, boolean graphObjectSelected)
    {
      checkBox.setChecked(graphObjectSelected);
      int visible = (graphObjectSelected || selectionStrategy
          .shouldShowCheckBoxIfUnselected()) ? View.VISIBLE : View.GONE;
      checkBox.setVisibility(visible);
    }
  }
}
